/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.reflection.parameter;

import cn.hermit.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 1/6/2017
 */
public class AnnotationParameterNameFinder implements ParameterNameFinder {

    private final ParameterNameFinder fallback;

    public AnnotationParameterNameFinder() {
        this(new NullParameterNameFinder());
    }

    public AnnotationParameterNameFinder(ParameterNameFinder fallback) {
        this.fallback = fallback;
    }

    public String[] findParameterNames(AccessibleObject methodOrConstructor) {
        return findParameterNames(methodOrConstructor, true);
    }

    public String[] findParameterNames(AccessibleObject methodOrCtor, boolean throwExceptionIfMissing) {
        // Oh for some commonality between Constructor and Method !!
        Class<?>[] types = null;
        Class<?> declaringClass = null;
        String name = null;
        Annotation[][] anns = null;
        if (methodOrCtor instanceof Method) {
            Method method = (Method) methodOrCtor;
            types = method.getParameterTypes();
            name = method.getName();
            declaringClass = method.getDeclaringClass();
            anns = method.getParameterAnnotations();
        } else {
            Constructor<?> constructor = (Constructor<?>) methodOrCtor;
            types = constructor.getParameterTypes();
            declaringClass = constructor.getDeclaringClass();
            name = "<init>";
            anns = constructor.getParameterAnnotations();
        }

        if (types.length == 0) {
            return EMPTY_NAMES;
        }


        final String[] names = new String[types.length];
        boolean allDone = true;
        for (int i = 0; i < names.length; i++) {
            for (int j = 0; j < anns[i].length; j++) {
                Annotation ann = anns[i][j];
                if (isNamed(ann)) {
                    names[i] = getNamedValue(ann);
                    break;
                }
            }
            if (names[i] == null) {
                allDone = false;
            }

        }

        // fill in blanks from fallback if possible.
        if (!allDone) {
            allDone = true;
            String[] altNames = fallback.findParameterNames(methodOrCtor, false);
            if (altNames.length > 0) {
                for (int i = 0; i < names.length; i++) {
                    if (names[i] == null) {
                        if (altNames[i] != null) {
                            names[i] = altNames[i];
                        } else {
                            allDone = false;
                        }
                    }
                }
            } else {
                allDone = false;
            }
        }

        // error if applicable
        if (!allDone) {
            if (throwExceptionIfMissing) {
                throw new ParameterNamesNotFoundException("One or more @Named annotations missing for class '" + declaringClass.getName() + "', methodOrCtor " + name
                        + " and parameter types " + DefaultParameterNameFinder.getParameterTypeNamesCSV(types));
            } else {
                return EMPTY_NAMES;
            }
        }
        return names;
    }

    /**
     * Override this if you want something other than JSR 330's Named annotation.
     * <p>
     * <pre>return ((Named) ann).value();</pre>
     *
     * @param ann the annotation in question
     * @return a the name value.
     */
    protected String getNamedValue(Annotation ann) {
        Class<?> realType = ann.annotationType();
        if (Param.class == realType) {
            return ((Param) ann).name();
        } else if (cn.hermit.nio.http.server.rest.annotation.Param.class == realType) {
            return ((cn.hermit.nio.http.server.rest.annotation.Param) ann).name();
        }
        try {
            if ("javax.inject.Named".equals(realType.getName())) {
                return (String) realType.getDeclaredMethod("value").invoke(ann);
            } else if ("io.swagger.annotations.ApiParam".equals(realType.getName())) {
                String name = (String) realType.getDeclaredMethod("name").invoke(ann);
                if (StringUtils.isBlank(name)) {
                    name = (String) realType.getDeclaredMethod("value").invoke(ann);
                }
                return name;
            } else if ("org.springframework.web.bind.annotation.RequestParam".equals(realType.getName())) {
                String name = (String) realType.getDeclaredMethod("name").invoke(ann);
                if (StringUtils.isBlank(name)) {
                    name = (String) realType.getDeclaredMethod("value").invoke(ann);
                }
                return name;
            }
        } catch (Exception e) {
            //ignored
        }
        return null;
    }


    /**
     * Override this if you want something other than JSR 330's Named annotation.
     * <p>
     * <pre>return ann instanceof Named;</pre>
     *
     * @param ann the annotation in question
     * @return whether it is the annotation holding the parameter name
     */
    protected boolean isNamed(Annotation ann) {
        Class<?> realType = ann.annotationType();
        return Param.class == realType
                || cn.hermit.nio.http.server.rest.annotation.Param.class == realType
                || "javax.inject.Named".equals(realType.getName())
                || "io.swagger.annotations.ApiParam".equals(realType.getName())
                || "org.springframework.web.bind.annotation.RequestParam".equals(realType.getName());
    }
}
